<?php
// $Id: imgcode.php 2524 2009-05-27 05:30:30Z jerry $


/**
 * 定义 Helper_ImgCode 类、Helper_ImgCodeSimple 类和 Helper_ImgCodeTTF 类
 *
 * @link http://qeephp.com/
 * @copyright Copyright (c) 2006-2009 Qeeyuan Inc. {@link http://www.qeeyuan.com}
 * @license New BSD License {@link http://qeephp.com/license/}
 * @version $Id: imgcode.php 2524 2009-05-27 05:30:30Z jerry $
 * @package helper
 */

/**
 * Helper_ImgCode 类提供验证码生成和检验的接口
 *
 * @author YuLei Liao <liaoyulei@qeeyuan.com>
 * @version $Id: imgcode.php 2524 2009-05-27 05:30:30Z jerry $
 * @package helper
 */
class Helper_ImgCode {
	/**
	 * 验证码的配置
	 *
	 * @var array
	 */
	private static $_config = array (// 用什么键名在 session 中保存验证码
	'imgcode_session_key' => '_IMGCODE', 

	// 从 session 中读取的验证码
	'imgcode_session_value' => null, 

	// 用什么键名在 session 中保存验证码过期时间
	'expired_session_key' => '_IMGCODE_EXPIRED', 

	// 验证码过期时间
	'expired_session_value' => null );
	
	/**
	 * 利用 GD 库产生验证码图像，并封装为 QView_Output 对象
	 *
	 * $style 参数值不同时，$options 参数包含的选项也不同。
	 *
	 * 用法：
	 * @code php
	 * // 控制器文件
	 * class Controller_Default
	 * {
	 * function actionImgcode()
	 * {
	 * // 在控制器中用下列代码返回一个图像验证码
	 * return Helper_ImgCode::create(6, 900, 'simple');
	 * }
	 * }
	 *
	 * // 模板文件中使用下列代码引用一个图像验证码
	 * <img src="<?php echo url('default/imgcode'); ?>" border="0" />
	 * @endcode
	 *
	 * @param int $length 验证码的长度
	 * @param int $lefttime 验证码的有效期
	 * @param string $style 验证码的样式
	 * @param array $options 具体验证码样式的选项
	 *
	 * @return QView_Output 包含验证码图像的输出对象
	 */
	static function create($length = 4, $lefttime = 900, $style = 'simple', array $options = null) {
		$class_name = 'Helper_ImgCode' . ucfirst ( strtolower ( preg_replace ( '/[^a-z0-9_]+/i', '', $style ) ) );
		$options = ( array ) $options;
		$options ['code_length'] = $length;
		$imgcode_obj = new $class_name ( $options );
		
		$code = $imgcode_obj->generateCode ();
		self::_writeImgcodeToSession ( $code, $lefttime );
		return $imgcode_obj->generateImage ( $code );
	}
	
	/**
	 * 比较输入的验证码是否和 session 中保存的验证码一致（不区分大小写）
	 *
	 * 用法：
	 * @code php
	 * // 控制器文件
	 * class Controller_Default
	 * {
	 * function actionLogin()
	 * {
	 * if (Helper_ImgCode::isValid($this->_context->imgcode))
	 * {
	 * .... 比对通过
	 * }
	 *
	 * ....
	 * }
	 * }
	 * @endcode
	 *
	 * @param string $code 要比对的验证码
	 * @param boolean $clean_session 是否在比对通过后清理 session 中保存的验证码
	 * @param boolean $case_sensitive 是否区分大小写
	 *
	 * @return boolean 比对结果
	 */
	static function isValid($code, $clean_session = false, $case_sensitive = false) {
		$code_in_session = self::_readImgcodeFromSession ();
		
		if (strlen ( $code_in_session ) == 0 || strlen ( $code ) == 0) {
			return false;
		}
		if ($case_sensitive) {
			$ret = ( string ) $code_in_session == ( string ) $code;
		} else {
			$ret = strtolower ( $code_in_session ) == strtolower ( $code );
		}
		
		if ($ret && $clean_session) {
			self::_cleanImgcodeFromSession ();
		}
		
		return $ret;
	}
	
	/**
	 * 清除 session 中的验证码信息
	 */
	static function clean() {
		self::_cleanImgcodeFromSession ();
	}
	
	/**
	 * 将 16 进制颜色值转换为 rgb 值
	 *
	 * 用法：
	 * @code php
	 * $color = '#369';
	 * list($r, $g, $b) = Helper_Image::hex2rgb($color);
	 * echo "red: {$r}, green: {$g}, blue: {$b}";
	 * @endcode
	 *
	 * @param string $color 颜色值
	 * @param string $default 使用无效颜色值时返回的默认颜色
	 *
	 * @return array 由 RGB 三色组成的数组
	 */
	static function hex2rgb($color, $default = 'ffffff') {
		$hex = trim ( $color, '#&Hh' );
		$len = strlen ( $hex );
		if ($len == 3) {
			$hex = "{$hex[0]}{$hex[0]}{$hex[1]}{$hex[1]}{$hex[2]}{$hex[2]}";
		} elseif ($len < 6) {
			$hex = $default;
		}
		$dec = hexdec ( $hex );
		return array (($dec >> 16) & 0xff, ($dec >> 8) & 0xff, $dec & 0xff );
	}
	
	/**
	 * 写入验证码和验证码过期时间到 session
	 *
	 * @param string $code 要写入 session 的验证码
	 * @param int $lefttime 验证码的有效期
	 */
	private static function _writeImgcodeToSession($code, $lefttime) {
		if (isset ( $_SESSION )) {
			$_SESSION [self::$_config ['imgcode_session_key']] = $code;
			$_SESSION [self::$_config ['expired_session_key']] = CURRENT_TIMESTAMP + intval ( $lefttime );
		}
	}
	
	/**
	 * 从 session 取得验证码和验证码过期时间
	 */
	private static function _readImgcodeFromSession() {
		if (! isset ( $_SESSION )) {
			return false;
		}
		
		$key = self::$_config ['imgcode_session_key'];
		$imgcode = isset ( $_SESSION [$key] ) ? $_SESSION [$key] : '';
		$key = self::$_config ['expired_session_key'];
		$expired = isset ( $_SESSION [$key] ) ? $_SESSION [$key] : 0;
		
		if (CURRENT_TIMESTAMP >= $expired)
			return false;
		return $imgcode;
	}
	
	/**
	 * 从 session 中清除验证码和验证码过期时间
	 */
	private static function _cleanImgcodeFromSession() {
		if (isset ( $_SESSION )) {
			$key = self::$_config ['imgcode_session_key'];
			unset ( $_SESSION [$key] );
			$key = self::$_config ['expired_session_key'];
			unset ( $_SESSION [$key] );
		}
	}
}

/**
 * Helper_ImgCodeSimple 实现了一个简单样式的验证码
 *
 * @author YuLei Liao <liaoyulei@qeeyuan.com>
 * @version $Id: imgcode.php 2524 2009-05-27 05:30:30Z jerry $
 * @package helper
 */
class Helper_ImgCodeSimple {
	/**
	 * 生成验证码需要的选项
	 *
	 * @var array
	 */
	protected $_options = array (// 验证码类型
	'code_type' => 2, 

	// 验证码长度
	'code_length' => 4, 

	// 字符上下浮动像素
	'float_pixel' => 6, 

	// 图像类型
	'image_type' => IMAGETYPE_JPEG, 

	// 字体，如果为 0-5，则使用 GD 库内置的字体
	// 如果使用自定义字体，font 必须指定为字体文件的绝对路径
	'font' => 5, 

	// 字体颜色
	'color' => '0xffffff', 

	// 背景色
	'bgcolor' => '0x666666', 

	// 验证码图片边界颜色
	'bdcolor' => '0x000000', 

	// 内空
	'padding' => 2, 

	// 边界线
	'border' => 1 );
	
	/**
	 * 构造函数
	 *
	 * @param array $options 生成验证码的选项
	 */
	function __construct(array $options) {
		$this->_options = array_merge ( $this->_options, $options );
	}
	
	/**
	 * 生成验证码
	 *
	 * @return string
	 */
	function generateCode() {
		$code_type = intval ( $this->_options ['code_type'] );
		$code_length = intval ( $this->_options ['code_length'] );
		if ($code_length <= 0) {
			$code_length = 4;
		}
		
		switch ($code_type) {
			case 0 :
				$seed = '0123456789';
				break;
			case 1 :
				$seed = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz';
				break;
			default :
				$seed = '346789ABCDEFGHJKLMNPQRTUVWXYabcdefghjklmnpqrtuvwxy';
		}
		$code = '';
		$len = strlen ( $seed ) - 1;
		for($i = 0; $i < $code_length; $i ++) {
			$code .= substr ( $seed, mt_rand ( 0, $len ), 1 );
		}
		
		return $code;
	}
	
	/**
	 * 生成验证码图像，返回 QView_Output 对象
	 *
	 * $param string $code
	 *
	 * @return QView_Output
	 */
	function generateImage($code) {
		// 根据选项确定绘图选项
		$padding = intval ( $this->_options ['padding'] );
		if ($padding < 0) {
			$padding = 0;
		}
		$color = $this->_options ['color'];
		$bgcolor = $this->_options ['bgcolor'];
		$border = $this->_options ['border'];
		$bdcolor = $this->_options ['bdcolor'];
		$float_pixel = intval ( $this->_options ['float_pixel'] );
		
		// 确定要使用的字体
		if (is_int ( $this->_options ['font'] )) {
			$font = intval ( $this->_options ['font'] );
			if ($font < 0 || $font > 5) {
				$font = 5;
			}
		} else {
			$font = imageloadfont ( $this->_options ['font'] );
		}
		
		// 确定字体宽度和高度
		$font_width = imagefontwidth ( $font );
		$font_height = imagefontheight ( $font );
		
		// 确定图像的宽度和高度
		$width = $font_width * strlen ( $code ) + $padding * 2 + $border * 2 + 1;
		$height = $font_height + $padding * 2 + $border * 2 + 1 + $float_pixel;
		
		// 创建图像
		$img = imagecreate ( $width, $height );
		
		// 绘制边框
		if ($border) {
			list ( $r, $g, $b ) = Helper_ImgCode::hex2rgb ( $bdcolor );
			$color = imagecolorallocate ( $img, $r, $g, $b );
			imagefilledrectangle ( $img, 0, 0, $width, $height, $color );
		}
		
		// 绘制背景
		list ( $r, $g, $b ) = Helper_ImgCode::hex2rgb ( $bgcolor );
		$color = imagecolorallocate ( $img, $r, $g, $b );
		imagefilledrectangle ( $img, $border, $border, $width - $border - 1, $height - $border - 1, $color );
		
		// 绘制文字
		list ( $r, $g, $b ) = Helper_ImgCode::hex2rgb ( $color );
		$color = imagecolorallocate ( $img, $r, $g, $b );
		
		for($i = 0, $max = strlen ( $code ); $i < $max; $i ++) {
			imagestring ( $img, $font, $padding + $border + $font_width * $i, $padding + $border + mt_rand ( 0, $float_pixel ), $code [$i], $color );
		}
		
		$filename = 'imgcode-' . $code;
		ob_start ();
		
		// 输出图像
		switch (strtolower ( $this->_options ['image_type'] )) {
			case 'png' :
				$filename .= '.png';
				$mime = image_type_to_mime_type ( IMAGETYPE_PNG );
				imagepng ( $img );
				break;
			
			case 'gif' :
				$filename .= '.gif';
				$mime = image_type_to_mime_type ( IMAGETYPE_GIF );
				imagegif ( $img );
				break;
			
			case 'jpg' :
			default :
				$filename .= '.jpg';
				$mime = image_type_to_mime_type ( IMAGETYPE_JPEG );
				imagejpeg ( $img );
		}
		
		imagedestroy ( $img );
		unset ( $img );
		
		$output = new QView_Output ( $filename, $mime, ob_get_clean () );
		$output->contentDisposition ( 'inline' )->enableClientCache ( false );
		
		return $output;
	}
}

/**
 * Helper_ImgCodeTTF 使用 ttf 字体生成验证码
 *
 * @author YuLei Liao <liaoyulei@qeeyuan.com>
 * @version $Id: imgcode.php 2524 2009-05-27 05:30:30Z jerry $
 * @package helper
 */
class Helper_ImgCodeTTF {
	/**
	 * 生成验证码需要的选项
	 *
	 * @var array
	 */
	protected $_options = array (// 验证码长度
	'code_length' => 6, 

	// 验证码图像宽度
	'width' => 170, 

	// 验证码图像高度
	'height' => 90, 

	// 字符上下浮动像素
	'float_pixel' => 10, 

	// 旋转角度
	'max_angle' => 35, 

	// 字体大小
	'font_size' => 50, 

	// 字体最低间距
	'font_space' => 0, 

	// 图像类型
	'image_type' => IMAGETYPE_JPEG, 

	// 字体，如果为 0-5，则使用 QeePHP 自带的字体
	// 如果使用自定义字体，font 必须指定为字体文件的绝对路径
	'font' => 0, 

	// 字体颜色
	'color' => 'fff', 

	// 背景色
	'bgcolor' => '333', 

	// 验证码图片边界颜色
	'bdcolor' => 'ccc', 

	// 内空
	'padding' => 5, 

	// 边界线
	'border' => 1 );
	
	/**
	 * 内置的字体列表
	 *
	 * @var array
	 */
	protected static $_font_list = array ('alpha_thin.ttf' );
	
	/**
	 * 构造函数
	 *
	 * @param array $options
	 * 生成验证码的选项
	 */
	function __construct(array $options) {
		$this->_options = array_merge ( $this->_options, $options );
	}
	
	/**
	 * 生成验证码
	 *
	 * @return string
	 */
	function generateCode() {
		$code_length = intval ( $this->_options ['code_length'] );
		if ($code_length <= 0) {
			$code_length = 4;
		}
		$seed = '346789ABCDEFGHJKLMN346789PQRTUVWXYabcdefghj346789klmnpqrt346789uvwxy';
		
		$code = '';
		$len = strlen ( $seed ) - 1;
		for($i = 0; $i < $code_length; $i ++) {
			$code .= substr ( $seed, mt_rand ( 0, $len ), 1 );
		}
		
		return $code;
	}
	
	/**
	 * 生成验证码图像，返回 QView_Output 对象
	 *
	 * $param string $code
	 *
	 * @return QView_Output
	 */
	function generateImage($code) {
		// 确定要使用的字体
		if (isset ( self::$_font_list [$this->_options ['font']] )) {
			$font = Q_DIR . '/_resources/' . self::$_font_list [$this->_options ['font']];
		} else {
			$font = $this->_options ['font'];
		}
		
		// 确定图像的宽度和高度
		$border = $this->_options ['border'];
		$float_pixel = intval ( $this->_options ['float_pixel'] );
		$padding = intval ( $this->_options ['padding'] );
		
		$width = intval ( $this->_options ['width'] ) + $padding * 2 + $border * 2 + 1;
		$width *= 2;
		$height = intval ( $this->_options ['height'] ) + $padding * 2 + $border * 2 + 1 + $float_pixel;
		
		// 创建图像
		$img = imagecreatetruecolor ( $width, $height );
		
		// 绘制背景
		list ( $r, $g, $b ) = Helper_ImgCode::hex2rgb ( $this->_options ['bgcolor'] );
		imagefilledrectangle ( $img, $border, $border, $width - $border - 1, $height - $border - 1, imagecolorallocate ( $img, $r, $g, $b ) );
		
		// 绘制文字
		$max_angle = intval ( $this->_options ['max_angle'] );
		
		$x = $padding + $padding + $border;
		
		$bottom = $height - $padding + $border - $float_pixel * 2;
		$font_space = $this->_options ['font_space'];
		$color_arr = Helper_ImgCode::hex2rgb ( $this->_options ['color'] );
		
		$arr = array ();
		$font_size = $this->_options ['font_size'];
		$min_r = $r + 50;
		$min_g = $g + 50;
		$min_b = $b + 50;
		for($i = 0, $max = strlen ( $code ); $i < $max; $i ++) {
			$arr [$i] ['font_size'] = (mt_rand ( 50, 150 ) / 100) * $font_size;
			$arr [$i] ['angle'] = rand ( 0, $max_angle ) - $max_angle / 2;
			list ( $r, $g, $b ) = $color_arr;
			$r = ($r + rand ( 0, 2550 )) % (255 - $min_r) + $min_r;
			$g = ($g + rand ( 0, 2550 )) % (255 - $min_g) + $min_g;
			$b = ($b + rand ( 0, 2550 )) % (255 - $min_b) + $min_b;
			$arr [$i] ['color'] = imagecolorallocate ( $img, $r, $g, $b );
		}
		
		for($i = 0; $i < $max; $i ++) {
			$x += $font_space;
			$y = $bottom;
			
			list ( , , $x ) = imagettftext ( $img, $arr [$i] ['font_size'], $arr [$i] ['angle'], $x, $y, $arr [$i] ['color'], $font, $code [$i] );
			imagecolordeallocate ( $img, $arr [$i] ['color'] );
		}
		
		$new_width = intval ( $this->_options ['width'] ) + $padding * 2 + $border * 2 + 1;
		$img_output = imagecreatetruecolor ( $new_width, $height );
		imagecopyresampled ( $img_output, $img, 0, 0, 0, 0, $new_width, $height, $x + $padding + $border, $height );
		imagedestroy ( $img );
		
		// 绘制边框
		if ($border) {
			list ( $r, $g, $b ) = Helper_ImgCode::hex2rgb ( $this->_options ['bdcolor'] );
			imagerectangle ( $img_output, 0, 0, $width, $height, imagecolorallocate ( $img_output, $r, $g, $b ) );
		}
		
		$filename = 'imgcode-' . $code;
		ob_start ();
		
		// 输出图像
		switch (strtolower ( $this->_options ['image_type'] )) {
			case 'png' :
				$filename .= '.png';
				$mime = image_type_to_mime_type ( IMAGETYPE_PNG );
				imagepng ( $img_output );
				break;
			
			case 'gif' :
				$filename .= '.gif';
				$mime = image_type_to_mime_type ( IMAGETYPE_GIF );
				imagegif ( $img_output );
				break;
			
			case 'jpg' :
			default :
				$filename .= '.jpg';
				$mime = image_type_to_mime_type ( IMAGETYPE_JPEG );
				imagejpeg ( $img_output );
		}
		
		imagedestroy ( $img_output );
		
		$output = new QView_Output ( $filename, $mime, ob_get_clean () );
		$output->enableClientCache ( false );
		
		return $output;
	}
}

